/*
 *   This program is free software: you can redistribute it and/or modify
 *   it under the terms of the GNU General Public License as published by
 *   the Free Software Foundation, either version 3 of the License, or
 *   (at your option) any later version.
 *
 *   This program is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *   GNU General Public License for more details.
 *
 *   You should have received a copy of the GNU General Public License
 *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

/*
 * CSV.java
 * Copyright (C) 2009-2012 University of Waikato, Hamilton, New Zealand
 */

package weka.classifiers.evaluation.output.prediction;

import java.util.Collections;
import java.util.Enumeration;
import java.util.Vector;

import weka.classifiers.Classifier;
import weka.core.Instance;
import weka.core.Option;
import weka.core.Utils;

/**
 * <!-- globalinfo-start --> Outputs the predictions as CSV.
 * <p/>
 * <!-- globalinfo-end -->
 * 
 * <!-- options-start --> Valid options are:
 * <p/>
 * 
 * <pre>
 * -p &lt;range&gt;
 *  The range of attributes to print in addition to the classification.
 *  (default: none)
 * </pre>
 * 
 * <pre>
 * -distribution
 *  Whether to turn on the output of the class distribution.
 *  Only for nominal class attributes.
 *  (default: off)
 * </pre>
 * 
 * <pre>
 * -decimals &lt;num&gt;
 *  The number of digits after the decimal point.
 *  (default: 3)
 * </pre>
 * 
 * <pre>
 * -file &lt;path&gt;
 *  The file to store the output in, instead of outputting it on stdout.
 *  Gets ignored if the supplied path is a directory.
 *  (default: .)
 * </pre>
 * 
 * <pre>
 * -suppress
 *  In case the data gets stored in a file, then this flag can be used
 *  to suppress the regular output.
 *  (default: not suppressed)
 * </pre>
 * 
 * <pre>
 * -use-tab
 *  Whether to use TAB as separator instead of comma.
 *  (default: comma)
 * </pre>
 * 
 * <!-- options-end -->
 * 
 * @author fracpete (fracpete at waikato dot ac dot nz)
 * @version $Revision$
 */
public class CSV extends AbstractOutput {

    /** for serialization. */
    private static final long serialVersionUID = 3401604538169573720L;

    /** the delimiter. */
    protected String m_Delimiter = ",";

    /**
     * Returns a string describing the output generator.
     * 
     * @return a description suitable for displaying in the GUI
     */
    @Override
    public String globalInfo() {
        return "Outputs the predictions as CSV.";
    }

    /**
     * Returns a short display text, to be used in comboboxes.
     * 
     * @return a short display text
     */
    @Override
    public String getDisplay() {
        return "CSV";
    }

    /**
     * Returns an enumeration of all the available options..
     * 
     * @return an enumeration of all available options.
     */
    @Override
    public Enumeration<Option> listOptions() {
        Vector<Option> result;

        result = new Vector<Option>();

        result.addElement(new Option("\tWhether to use TAB as separator instead of comma.\n" + "\t(default: comma)", "use-tab", 0, "-use-tab"));

        result.addAll(Collections.list(super.listOptions()));

        return result.elements();
    }

    /**
     * Sets the OptionHandler's options using the given list. All options will be
     * set (or reset) during this call (i.e. incremental setting of options is not
     * possible).
     * 
     * @param options the list of options as an array of strings
     * @throws Exception if an option is not supported
     */
    @Override
    public void setOptions(String[] options) throws Exception {
        setUseTab(Utils.getFlag("use-tab", options));
        super.setOptions(options);
    }

    /**
     * Gets the current option settings for the OptionHandler.
     * 
     * @return the list of current option settings as an array of strings
     */
    @Override
    public String[] getOptions() {
        Vector<String> result = new Vector<String>();

        if (getUseTab()) {
            result.add("-use-tab");
        }

        Collections.addAll(result, super.getOptions());

        return result.toArray(new String[result.size()]);
    }

    /**
     * Sets whether to use tab instead of comma as separator.
     * 
     * @param value true if tab is to be used
     */
    public void setUseTab(boolean value) {
        if (value) {
            m_Delimiter = "\t";
        } else {
            m_Delimiter = ",";
        }
    }

    /**
     * Returns whether tab is used as separator.
     * 
     * @return true if tab is used instead of comma
     */
    public boolean getUseTab() {
        return m_Delimiter.equals("\t");
    }

    /**
     * Returns the tip text for this property.
     * 
     * @return tip text for this property suitable for displaying in the GUI
     */
    public String useTabTipText() {
        return "Whether to use TAB instead of COMMA as column separator.";
    }

    /**
     * Performs the actual printing of the header.
     */
    @Override
    protected void doPrintHeader() {
        if (m_Header.classAttribute().isNominal()) {
            if (m_OutputDistribution) {
                append("inst#" + m_Delimiter + "actual" + m_Delimiter + "predicted" + m_Delimiter + "error" + m_Delimiter + "distribution");
                for (int i = 1; i < m_Header.classAttribute().numValues(); i++) {
                    append(m_Delimiter);
                }
            } else {
                append("inst#" + m_Delimiter + "actual" + m_Delimiter + "predicted" + m_Delimiter + "error" + m_Delimiter + "prediction");
            }
        } else {
            append("inst#" + m_Delimiter + "actual" + m_Delimiter + "predicted" + m_Delimiter + "error");
        }

        if (m_Attributes != null) {
            append(m_Delimiter);
            boolean first = true;
            for (int i = 0; i < m_Header.numAttributes(); i++) {
                if (i == m_Header.classIndex()) {
                    continue;
                }

                if (m_Attributes.isInRange(i)) {
                    if (!first) {
                        append(m_Delimiter);
                    }
                    append(m_Header.attribute(i).name());
                    first = false;
                }
            }
        }

        append("\n");
    }

    /**
     * Builds a string listing the attribute values in a specified range of indices,
     * separated by commas and enclosed in brackets.
     * 
     * @param instance the instance to print the values from
     * @return a string listing values of the attributes in the range
     */
    protected String attributeValuesString(Instance instance) {
        StringBuffer text = new StringBuffer();
        if (m_Attributes != null) {
            m_Attributes.setUpper(instance.numAttributes() - 1);
            boolean first = true;
            for (int i = 0; i < instance.numAttributes(); i++) {
                if (m_Attributes.isInRange(i) && i != instance.classIndex()) {
                    if (!first) {
                        text.append(m_Delimiter);
                    }
                    text.append(instance.toString(i));
                    first = false;
                }
            }
        }
        return text.toString();
    }

    /**
     * Store the prediction made by the classifier as a string.
     * 
     * @param dist  the distribution to use
     * @param inst  the instance to generate text from
     * @param index the index in the dataset
     * @throws Exception if something goes wrong
     */
    @Override
    protected void doPrintClassification(double[] dist, Instance inst, int index) throws Exception {
        int prec = m_NumDecimals;

        Instance withMissing = (Instance) inst.copy();
        withMissing.setDataset(inst.dataset());

        double predValue = 0;
        if (Utils.sum(dist) == 0) {
            predValue = Utils.missingValue();
        } else {
            if (inst.classAttribute().isNominal()) {
                predValue = Utils.maxIndex(dist);
            } else {
                predValue = dist[0];
            }
        }

        // index
        append("" + (index + 1));

        if (inst.dataset().classAttribute().isNumeric()) {
            // actual
            if (inst.classIsMissing()) {
                append(m_Delimiter + "?");
            } else {
                append(m_Delimiter + Utils.doubleToString(inst.classValue(), prec));
            }
            // predicted
            if (Utils.isMissingValue(predValue)) {
                append(m_Delimiter + "?");
            } else {
                append(m_Delimiter + Utils.doubleToString(predValue, prec));
            }
            // error
            if (Utils.isMissingValue(predValue) || inst.classIsMissing()) {
                append(m_Delimiter + "?");
            } else {
                append(m_Delimiter + Utils.doubleToString(predValue - inst.classValue(), prec));
            }
        } else {
            // actual
            append(m_Delimiter + ((int) inst.classValue() + 1) + ":" + inst.toString(inst.classIndex()));
            // predicted
            if (Utils.isMissingValue(predValue)) {
                append(m_Delimiter + "?");
            } else {
                append(m_Delimiter + ((int) predValue + 1) + ":" + inst.dataset().classAttribute().value((int) predValue));
            }
            // error?
            if (!Utils.isMissingValue(predValue) && !inst.classIsMissing() && ((int) predValue + 1 != (int) inst.classValue() + 1)) {
                append(m_Delimiter + "+");
            } else {
                append(m_Delimiter + "");
            }
            // prediction/distribution
            if (m_OutputDistribution) {
                if (Utils.isMissingValue(predValue)) {
                    append(m_Delimiter + "?");
                } else {
                    append(m_Delimiter);
                    for (int n = 0; n < dist.length; n++) {
                        if (n > 0) {
                            append(m_Delimiter);
                        }
                        if (n == (int) predValue) {
                            append("*");
                        }
                        append(Utils.doubleToString(dist[n], prec));
                    }
                }
            } else {
                if (Utils.isMissingValue(predValue)) {
                    append(m_Delimiter + "?");
                } else {
                    append(m_Delimiter + Utils.doubleToString(dist[(int) predValue], prec));
                }
            }
        }

        // attributes
        if (m_Attributes != null) {
            append(m_Delimiter + attributeValuesString(withMissing));
        }
        append("\n");
    }

    /**
     * Store the prediction made by the classifier as a string.
     * 
     * @param classifier the classifier to use
     * @param inst       the instance to generate text from
     * @param index      the index in the dataset
     * @throws Exception if something goes wrong
     */
    @Override
    protected void doPrintClassification(Classifier classifier, Instance inst, int index) throws Exception {

        double[] d = classifier.distributionForInstance(inst);
        doPrintClassification(d, inst, index);
    }

    /**
     * Does nothing.
     */
    @Override
    protected void doPrintFooter() {
    }
}
